using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;
using System.Windows;
using System.Diagnostics;
using System.Threading;
using System.ComponentModel;
using System.Windows.Media.Imaging;
using System.Data.SQLite;
using System.Windows.Media;
using System.Xml.Linq;
using System.Net;
using System.Xml;
using Microsoft.Win32;

namespace KeyboardImageViewer
{
	public static class ImageManager
	{
		private const string ConfigFileName = "KeyboardImageViewer.txt";
        private const int FileChunkSize = 10;
        private const int HashChunkSize = 50;
        private const int SummaryChunkSize = 1;
        private const int HashVisibleThreshold = 100;
        private const int SummaryVisibleThreshold = 1;

        private static string[] extensions = new string[] { "jpg", "jpeg", "gif", "png", "bmp", "tif" };

        private static MainWindow mainForm;
        private static ViewerDatabaseConfig databaseConfig;

        private static SQLiteConnection connection;

        private static Queue<IProcessTask> imagesToProcess;
        private static int chunkSize;
        private static object imagesToProcessLock = new object();
        private static int chunksCompleted;
        private static int numBackgroundWorkers;
        private static int workersCompleted;
        private static bool showingProgress;
        private static int numImagesToProcess;

        private static List<string> movedDirectoriesToProcess;

        public static void Initialize(MainWindow mainWindow)
        {
            mainForm = mainWindow;

            imagesToProcess = new Queue<IProcessTask>();

            bool newDataFile = !File.Exists(Database.DatabaseFile);
            bool oldConfigFound = false;

            if (newDataFile)
            {
                DirectorySet sourceDirectories = null;

                Welcome welcomeScreen = new Welcome();
                if ((bool)welcomeScreen.ShowDialog())
                {
                    sourceDirectories = welcomeScreen.SourceDirectories;
                }
                else
                {
                    Environment.Exit(0);
                }

                connection = new SQLiteConnection(Database.ConnectionString);
                connection.Open();

                foreach (BaseDirectory baseDirectory in sourceDirectories.BaseDirectories)
                {
                    if (File.Exists(Path.Combine(baseDirectory.Path, ConfigFileName)))
                    {
                        // Create tables at DB version 2
                        Database.CreateTables(connection, false);

                        ViewerConfig oldConfig = ViewerConfig.FromFiles(Path.Combine(baseDirectory.Path, ConfigFileName));

                        Database.PopulateTablesFromOldConfig(connection, oldConfig, baseDirectory.Path);

                        oldConfigFound = true;
                        break;
                    }
                }

                if (!oldConfigFound)
                {
                    Database.CreateTables(connection);

                    Database.PopulateDefaultConfig(connection, sourceDirectories);
                }
            }

            if (connection == null)
            {
                connection = new SQLiteConnection(Database.ConnectionString);
                connection.Open();
            }

            UpdateHandler.HandlePendingUpdate(connection);

            if (!newDataFile || oldConfigFound)
            {
                DatabaseUpgrades.UpgradeIfNeeded(connection);
            }

            databaseConfig = new ViewerDatabaseConfig(connection);

            // Now that configuration has been initialized, check our update status.
            UpdateHandler.CheckUpdates();

            databaseConfig.ApplyWindowSize("Main", mainForm);

            QueryOperations.SetImageList(databaseConfig.StartingListId);

            RefreshFiles();
        }

		#region Public Properties

        public static bool ImageShowing
		{
			get
			{
                return ImageListControl.ImageList.Count > 0 && ImageCache.Current.Image != null;
			}
		}

        public static bool ImageLoading
        {
            get
            {
                return ImageListControl.ImageList.Count > 0 && ImageCache.Current.CallInProgress;
            }
        }

        public static SQLiteConnection Connection
        {
            get
            {
                return connection;
            }
        }

        public static string CurrentImageName
        {
            get
            {
                return Database.GetImageName(connection, ImageListControl.CurrentImageId);
            }
        }

        public static bool SearchFileName
		{
			get { return databaseConfig.SearchFileName; }
		}

        public static bool SearchTags
		{
            get { return databaseConfig.SearchTags; }
		}

        public static List<string> CurrentTags
        {
            get
            {
                return Database.GetTags(connection, ImageListControl.CurrentImageId);
            }
        }

        public static List<KeyValuePair<string, string>> DependencyList
        {
            get
            {
                return databaseConfig.TagDependencies;
            }

            set
            {
                databaseConfig.TagDependencies = value;

                Database.RebuildDependentTags(connection, value);
            }
        }

        public static ViewerDatabaseConfig Config
		{
			get { return databaseConfig; }
		}

        public static MainWindow MainForm
        {
            get { return mainForm; }
        }

		#endregion

		#region Public Methods

        public static void SaveWindowSize()
        {
            if (mainForm.WindowState == WindowState.Normal)
            {
                databaseConfig.SaveWindowSize("Main", mainForm);
            }
        }

        public static void UpdateSettings(Settings settingsWindow)
        {
            bool updatesEnabled = settingsWindow.UpdatesEnabled;
            bool trackDuplicates = settingsWindow.TrackDuplicates;
            bool trackImageDuplicates = settingsWindow.TrackImageDuplicates;
            DirectorySet sourceDirectories = settingsWindow.SourceDirectories;
            List<string> movedDirectories = settingsWindow.MovedDirectories;
            FullscreenStart fullscreenStart = settingsWindow.FullscreenStart;
            bool overlayInfobar = settingsWindow.OverlayInfobar;

            if (databaseConfig.UpdatesEnabled != updatesEnabled)
            {
                databaseConfig.UpdatesEnabled = updatesEnabled;
                UpdateHandler.HandleUpdatedSettings(updatesEnabled);
            }

            databaseConfig.FullscreenStartSetting = fullscreenStart;
            databaseConfig.OverlayInfobar = overlayInfobar;
            mainForm.OverlayInfobar = overlayInfobar;

            int slideshowDelay;
            if (int.TryParse(settingsWindow.SlideshowDelaySeconds, out slideshowDelay) && slideshowDelay > 0)
            {
                databaseConfig.SlideshowDelaySeconds = slideshowDelay;
            }

            bool refreshFiles = false;
            bool refreshView = false;
            bool refreshIncludedFlag = false;

            if (ImageListControl.StopSlideshowIfRunning())
            {
                refreshView = true;
            }

            if (!databaseConfig.SourceDirectories.Equals(sourceDirectories))
            {
                databaseConfig.SourceDirectories = sourceDirectories;
                movedDirectoriesToProcess = movedDirectories;
                refreshIncludedFlag = true;
                refreshFiles = true;
            }

            if (databaseConfig.TrackDuplicates != trackDuplicates)
            {
                if (trackDuplicates)
                {
                    databaseConfig.TrackDuplicates = true;
                    refreshFiles = true;
                }
                else
                {
                    databaseConfig.TrackDuplicates = false;
                    refreshView = true;
                }
            }

            if (databaseConfig.TrackImageDuplicates != trackImageDuplicates)
            {
                databaseConfig.TrackImageDuplicates = trackImageDuplicates;

                if (trackDuplicates)
                {
                    refreshFiles = true;
                }
            }

            if (refreshIncludedFlag)
            {
                Database.RefreshIncludedFlag(connection, sourceDirectories);
            }

            if (refreshFiles)
            {
                RefreshFiles();
            }
            else if (refreshView)
            {
                mainForm.UpdateInfoBar();
            }
        }

        public static void ReloadFiles()
		{
			RefreshFiles();
		}

		#endregion

		#region Private Methods

        private static void RefreshFiles()
        {
            mainForm.AllowInput = false;

            List<FileInfo> fileList = GetAllImages(databaseConfig.SourceDirectories);

            List<OrphanedData> orphanedImages = new List<OrphanedData>();

            fileList.Sort(Utilities.FileInfoNameComparer);

            SQLiteCommand getIncludedItems = new SQLiteCommand("SELECT id, name, lastWriteTime, hash, images.lastRefreshDate AS imagesLastRefresh, summaries.lastRefreshDate AS summariesLastRefresh, ineligible, tags, rating FROM images LEFT JOIN summaries ON images.id = summaries.imageId WHERE included ORDER BY lower(name) ASC", connection);
            SQLiteDataReader reader = getIncludedItems.ExecuteReader();

            int numImagesToHash = 0;
            int numImagesToSummarize = 0;

            long newImageId = Database.GetLastInsertId(connection) + 1;

            using (SQLiteTransaction transaction = connection.BeginTransaction())
            {
                var updateWriteTimeTime = new SQLiteParameter();
                var updateWriteTimeId = new SQLiteParameter();

                SQLiteCommand updateWriteTime = new SQLiteCommand(connection);
                updateWriteTime.CommandText = "UPDATE images SET lastWriteTime = ? WHERE id = ?";
                updateWriteTime.Parameters.Add(updateWriteTimeTime);
                updateWriteTime.Parameters.Add(updateWriteTimeId);

                var updateNameName = new SQLiteParameter();
                var updateNameId = new SQLiteParameter();

                SQLiteCommand updateName = new SQLiteCommand(connection);
                updateName.CommandText = "UPDATE images SET name = ? WHERE id = ?";
                updateName.Parameters.Add(updateNameName);
                updateName.Parameters.Add(updateNameId);

                var insertNewImageId = new SQLiteParameter();
                var insertNewImageName = new SQLiteParameter();
                var insertNewImageLastWriteTime = new SQLiteParameter();
                var insertNewImageIncluded = new SQLiteParameter();

                SQLiteCommand insertNewImage = new SQLiteCommand(connection);
                insertNewImage.CommandText = "INSERT INTO images (id, name, lastWriteTime, included) VALUES (?, ?, ?, ?)";
                insertNewImage.Parameters.Add(insertNewImageId);
                insertNewImage.Parameters.Add(insertNewImageName);
                insertNewImage.Parameters.Add(insertNewImageLastWriteTime);
                insertNewImage.Parameters.Add(insertNewImageIncluded);

                insertNewImageIncluded.Value = true;

                var deleteOldImageId = new SQLiteParameter();
                SQLiteCommand deleteOldImage = new SQLiteCommand(connection);
                deleteOldImage.CommandText = "DELETE FROM images WHERE id = ?";
                deleteOldImage.Parameters.Add(deleteOldImageId);

                var deleteOldImageSummaryId = new SQLiteParameter();
                SQLiteCommand deleteOldImageSummary = new SQLiteCommand(connection);
                deleteOldImageSummary.CommandText = "DELETE FROM summaries WHERE imageId = ?";
                deleteOldImageSummary.Parameters.Add(deleteOldImageSummaryId);

                int i = 0;
                bool rowRead;

                while ((rowRead = reader.Read()) || i < fileList.Count)
                {
                    while (i < fileList.Count && (!rowRead || String.Compare(fileList[i].FullName.ToLowerInvariant(), reader.GetString("name").ToLowerInvariant()) < 0))
                    {
                        // Add new image to main table
                        insertNewImageId.Value = newImageId;
                        insertNewImageName.Value = fileList[i].FullName;
                        insertNewImageLastWriteTime.Value = fileList[i].LastWriteTime;

                        insertNewImage.ExecuteNonQuery();

                        // Schedule processing for new image
                        if (databaseConfig.TrackDuplicates)
                        {
                            if (databaseConfig.TrackImageDuplicates)
                            {
                                numImagesToHash++;
                                numImagesToSummarize++;

                                imagesToProcess.Enqueue(new SummaryAndHashTask(newImageId, fileList[i].FullName, fileList[i].LastWriteTime, true));
                            }
                            else
                            {
                                numImagesToHash++;

                                imagesToProcess.Enqueue(new HashTask(newImageId, fileList[i].FullName, fileList[i].LastWriteTime));
                            }
                        }

                        newImageId++;

                        i++;
                    }

                    if (!rowRead)
                    {
                        continue;
                    }

                    string databaseName = reader.GetString("name");
                    int databaseId = reader.GetInt32("id");

                    if (i >= fileList.Count || fileList[i].FullName.ToLowerInvariant() != databaseName.ToLowerInvariant())
                    {
                        // This item is orphaned, add it to the list to process later
                        OrphanedData orphanedData = new OrphanedData { Name = databaseName, ImageId = databaseId };

                        if (databaseConfig.TrackDuplicates && !reader.IsDBNull("hash"))
                        {
                            orphanedData.Hash = reader.GetString("hash");
                        }

                        if (!reader.IsDBNull("tags"))
                        {
                            orphanedData.Tags = reader.GetString("tags");
                        }

                        if (!reader.IsDBNull("rating"))
                        {
                            orphanedData.Rating = reader.GetInt32("rating");
                        }

                        orphanedImages.Add(orphanedData);

                        // Remove original database entry for item
                        deleteOldImageId.Value = databaseId;
                        deleteOldImage.ExecuteNonQuery();

                        deleteOldImageSummaryId.Value = databaseId;
                        deleteOldImageSummary.ExecuteNonQuery();
                    }
                    else
                    {
                        // Update LastWriteTime and ensure hash/image summary is up to date
                        if (reader.IsDBNull("lastWriteTime") || reader.GetDateTime("lastWriteTime") != fileList[i].LastWriteTime)
                        {
                            updateWriteTimeId.Value = databaseId;
                            updateWriteTimeTime.Value = fileList[i].LastWriteTime;

                            updateWriteTime.ExecuteNonQuery();
                        }

                        if (databaseName != fileList[i].FullName)
                        {
                            updateNameName.Value = fileList[i].FullName;
                            updateNameId.Value = databaseId;

                            updateName.ExecuteNonQuery();
                        }

                        if (databaseConfig.TrackDuplicates)
                        {
                            bool hashRefresh = false;
                            bool summaryRefresh = false;
                            bool newRow = false;

                            if (reader.IsDBNull("hash") || reader.GetDateTime("imagesLastRefresh") < fileList[i].LastWriteTime)
                            {
                                hashRefresh = true;
                                numImagesToHash++;
                            }

                            if (databaseConfig.TrackImageDuplicates)
                            {
                                bool ineligible = !reader.IsDBNull("ineligible") && reader.GetBoolean("ineligible");

                                if (!ineligible && (reader.IsDBNull("summariesLastRefresh") || reader.GetDateTime("summariesLastRefresh") < fileList[i].LastWriteTime))
                                {
                                    newRow = reader.IsDBNull("summariesLastRefresh");
                                    summaryRefresh = true;
                                    numImagesToSummarize++;
                                }
                            }

                            if (hashRefresh && summaryRefresh)
                            {
                                imagesToProcess.Enqueue(new SummaryAndHashTask(reader.GetInt32("id"), fileList[i].FullName, fileList[i].LastWriteTime, newRow));
                            }
                            else if (hashRefresh)
                            {
                                imagesToProcess.Enqueue(new HashTask(reader.GetInt32("id"), fileList[i].FullName, fileList[i].LastWriteTime));
                            }
                            else if (summaryRefresh)
                            {
                                imagesToProcess.Enqueue(new SummaryTask(reader.GetInt32("id"), fileList[i].FullName, fileList[i].LastWriteTime, newRow));
                            }
                        }

                        i++;
                    }
                }

                reader.Dispose();

                transaction.Commit();
            }

            if (numImagesToSummarize > 0)
            {
                chunkSize = SummaryChunkSize;
            }
            else
            {
                chunkSize = HashChunkSize;
            }

            if (imagesToProcess.Count > 0)
            {
                if (numImagesToSummarize > 0)
                {
                    showingProgress = imagesToProcess.Count >= SummaryVisibleThreshold;
                }
                else
                {
                    showingProgress = imagesToProcess.Count >= HashVisibleThreshold;
                }
            }
            else
            {
                showingProgress = false;
            }

            if (showingProgress)
            {
                mainForm.SetPicture(null);
                mainForm.ProgressBarVisible = true;
            }

            chunksCompleted = 0;
            workersCompleted = 0;
            numImagesToProcess = imagesToProcess.Count;

            int chunks = (int)Math.Ceiling(((double)numImagesToProcess) / chunkSize);
            numBackgroundWorkers = Math.Max(Math.Min(Environment.ProcessorCount, chunks), 1);

            for (int i = 0; i < numBackgroundWorkers; i++)
            {
                BackgroundWorker worker = new BackgroundWorker();
                worker.WorkerReportsProgress = true;
                worker.DoWork += new DoWorkEventHandler(worker_DoWork);
                worker.ProgressChanged += new ProgressChangedEventHandler(worker_ProgressChanged);
                worker.RunWorkerCompleted += delegate(object sender, RunWorkerCompletedEventArgs args)
                {
                    workersCompleted++;

                    if (workersCompleted >= numBackgroundWorkers)
                    {
                        FinishFilesRefresh(orphanedImages);

                        mainForm.AllowInput = true;
                    }
                };

                worker.RunWorkerAsync();
            }
        }

        private static void worker_DoWork(object sender, DoWorkEventArgs e)
        {
            BackgroundWorker worker = sender as BackgroundWorker;
            SQLiteConnection workerConnection = new SQLiteConnection(Database.ConnectionString);
            workerConnection.Open();

            List<IProcessTask> taskBatch;

            do
            {
                taskBatch = new List<IProcessTask>();

                lock (imagesToProcessLock)
                {
                    for (int i = 0; i < chunkSize && imagesToProcess.Count > 0; i++)
                    {
                        taskBatch.Add(imagesToProcess.Dequeue());
                    }
                }

                if (taskBatch.Count > 0)
                {
                    for (int i = 0; i < taskBatch.Count; i++)
                    {
                        taskBatch[i].Process();
                    }

                    using (SQLiteTransaction transaction = workerConnection.BeginTransaction())
                    {
                        for (int i = 0; i < taskBatch.Count; i++)
                        {
                            taskBatch[i].Commit(workerConnection);
                        }

                        transaction.Commit();
                    }

                    worker.ReportProgress(0);
                }
            } while (taskBatch.Count > 0);
        }

        private static void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            chunksCompleted++;
            if (showingProgress)
            {
                mainForm.ReportProgress(((double)(chunksCompleted * chunkSize * 100)) / numImagesToProcess);
            }
        }

        private static void FinishFilesRefresh(List<OrphanedData> orphanedImages)
        {
            if (showingProgress)
            {
                mainForm.ProgressBarVisible = false;
            }

            Database.ApplyOrphanedData(orphanedImages, connection, Config.TagDependencies, Config.TrackDuplicates);

            if (movedDirectoriesToProcess != null && movedDirectoriesToProcess.Count > 0)
            {
                List<OrphanedData> movedImages = new List<OrphanedData>();

                using (SQLiteTransaction transaction = connection.BeginTransaction())
                {
                    var deleteOldImageId = new SQLiteParameter();
                    SQLiteCommand deleteOldImage = new SQLiteCommand(connection);
                    deleteOldImage.CommandText = "DELETE FROM images WHERE id = ?";
                    deleteOldImage.Parameters.Add(deleteOldImageId);

                    var deleteOldImageSummaryId = new SQLiteParameter();
                    SQLiteCommand deleteOldImageSummary = new SQLiteCommand(connection);
                    deleteOldImageSummary.CommandText = "DELETE FROM summaries WHERE imageId = ?";
                    deleteOldImageSummary.Parameters.Add(deleteOldImageSummaryId);

                    foreach (string movedDirectory in movedDirectoriesToProcess)
                    {
                        SQLiteCommand getMissingData = new SQLiteCommand("SELECT id, name, tags, rating, hash FROM images WHERE name LIKE \"" + movedDirectory + "%\"", connection);
                        using (SQLiteDataReader reader = getMissingData.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                string tags = null;
                                if (!reader.IsDBNull("tags"))
                                {
                                    tags = reader.GetString("tags");
                                }
                                int rating = 0;
                                if (!reader.IsDBNull("rating"))
                                {
                                    rating = reader.GetInt32("rating");
                                }

                                string hash = null;
                                if (!reader.IsDBNull("hash"))
                                {
                                    hash = reader.GetString("hash");
                                }

                                bool hasTags = tags != null && tags.Length > 2;

                                if (hasTags || rating > 0)
                                {
                                    OrphanedData orphanedData = new OrphanedData { Name = reader.GetString("name") };

                                    if (hasTags)
                                    {
                                        orphanedData.Tags = tags;
                                    }

                                    if (rating > 0)
                                    {
                                        orphanedData.Rating = rating;
                                    }

                                    if (!string.IsNullOrEmpty(hash))
                                    {
                                        orphanedData.Hash = hash;
                                    }

                                    movedImages.Add(orphanedData);
                                }

                                int oldImageId = reader.GetInt32("id");
                                deleteOldImageId.Value = oldImageId;
                                deleteOldImage.ExecuteNonQuery();

                                deleteOldImageSummaryId.Value = oldImageId;
                                deleteOldImageSummary.ExecuteNonQuery();
                            }
                        }
                    }

                    transaction.Commit();
                }

                Database.ApplyOrphanedData(movedImages, connection, Config.TagDependencies, Config.TrackDuplicates);
            }

            Database.RebuildTagsSummary();

            ImageListControl.RunImageQueryForced();
        }

        private static List<FileInfo> GetAllImages(DirectorySet directorySet)
        {
            List<FileInfo> fileList = new List<FileInfo>();

            bool directoriesChanged = false;

            foreach (BaseDirectory baseDirectory in directorySet.BaseDirectories)
            {
                if (baseDirectory.Missing != !Directory.Exists(baseDirectory.Path))
                {
                    directoriesChanged = true;

                    baseDirectory.Missing = !Directory.Exists(baseDirectory.Path);
                }
            }

            if (directoriesChanged)
            {
                databaseConfig.SourceDirectories = directorySet;

                Database.RefreshIncludedFlag(connection, directorySet);
            }

            foreach (BaseDirectory baseDirectory in directorySet.BaseDirectories)
            {
                if (!baseDirectory.Missing)
                {
                    AddImages(baseDirectory, fileList, new DirectoryInfo(baseDirectory.Path));
                }
            }

            return fileList;
        }

        private static void AddImages(BaseDirectory baseDirectory, List<FileInfo> fileList, DirectoryInfo currentDir)
        {
            foreach (string extension in extensions)
            {
                fileList.AddRange(currentDir.GetFiles("*." + extension));
            }

            try
            {
                DirectoryInfo[] subDirectories = currentDir.GetDirectories();

                foreach (DirectoryInfo subDirectory in subDirectories)
                {
                    if (!baseDirectory.Exclusions.Contains(subDirectory.FullName.ToLowerInvariant()))
                    {
                        AddImages(baseDirectory, fileList, subDirectory);
                    }
                }
            }
            catch (UnauthorizedAccessException)
            {
                // Do nothing. We won't read images from inaccessible folders.
            }
        }

		#endregion
	}
}
